//
//  JsonValidator v 1.1
//  Russell Lowke, February 9th 2014
//
//  Copyright (c) 2013 Lowke Media
//  see http://www.lowkemedia.com for more information
//  see http://code.google.com/p/lowke/ for code repository
//
//  Permission is hereby granted, free of charge, to any person obtaining a 
//  copy of this software and associated documentation files (the "Software"), 
//  to deal in the Software without restriction, including without limitation 
//  the rights to use, copy, modify, merge, publish, distribute, sublicense, 
//  and/or sell copies of the Software, and to permit persons to whom the 
//  Software is furnished to do so, subject to the following conditions:
// 
//  The above copyright notice and this permission notice shall be included in 
//  all copies or substantial portions of the Software.
// 
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER  
//  DEALINGS IN THE SOFTWARE. 
//
//

package com.lowke.jsonValidator
{
    
    import com.lowke.Util;
    import com.lowke.dump.Dump;
    import com.lowke.jsonValidator.ClassPacker;
    import com.lowke.logAs.LogAs;
    
    import flash.utils.getQualifiedClassName;
    
    /**
     * @author Russell Lowke
     */
    public class JsonValidator
    {   
        private static const TRUE:String = "true";
        private static const FALSE:String = "false";
        
        public static const LOG_PREFIX:String                                           = "JPK";
        public static const WARNING_VARIABLE_BEING_SET_ALREADY_EXISTS:String            = "JPK01";
        public static const WARNING_CANT_GET_REQUIRED_STRING:String                     = "JPK02";
        public static const WARNING_CANT_GET_REQUIRED_FIELD:String                      = "JPK03";
        public static const WARNING_CANT_GET_REQUIRED_BOOLEAN:String                    = "JPK04";
        public static const WARNING_CANT_GET_REQUIRED_NUMBER:String                     = "JPK05";
        public static const WARNING_STRING_VALUE_NOT_LEGAL:String                       = "JPK06";
        public static const WARNING_DEFAULT_VALUE_NOT_LEGAL:String                      = "JPK07";
        public static const WARNING_STRING_VALUE_IS_ILLEGAL:String                      = "JPK08";
        public static const WARNING_DEFAULT_VALUE_IS_ILLEGAL:String                     = "JPK09";
        public static const WARNING_NUMBER_EXCEEDS_MAXIMUM:String                       = "JPK10";
        public static const WARNING_NUMBER_BELOW_MINIMUM:String                         = "JPK11";
        public static const WARNING_UINT_PASSED_AS_NEGATIVE:String                      = "JPK12";
        public static const WARNING_CANT_UNPACK_WITHOUT_CLASSNAME:String                = "JPK13";
        public static const WARNING_OBJECT_NOT_JSON:String                              = "JPK14";
        
		
		public static function getString(json:Object,
										 field:String,
										 fRequired:Boolean = false,
										 defaultValue:String = null,
										 fForceToLowerCase:Boolean = false,
										 legalValues:Vector.<String> = null,
										 illegalValues:Vector.<String> = null):String
		{   
			if (! Dump.isDynamicObject(json)) 
			{
				if (json) 
				{
					LogAs.warning("AssertJson.assertString() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON);
				}
				return defaultValue;
			}
			
			// get the string
			var string:String = json[field];
			
			// if required and has no value then give a warning
			if (fRequired && ! Dump.stringHasValue(string)) 
			{
				LogAs.warning("Can't get required String field " + Dump.toString(field) + ".\r\rjson:" + Dump.toString(json), WARNING_CANT_GET_REQUIRED_STRING);
			}
			
			if (fForceToLowerCase)
			{
				if (Util.stringHasValue(string))
				{
					string = string.toLowerCase();
				}
				if (Util.stringHasValue(defaultValue))
				{
					defaultValue = defaultValue.toLowerCase();
				}
			}
			
			// if no string set it to the default
			if (! string) 
			{
				string = defaultValue;
				
				// check that the default is legal
				if (illegalValues && illegalValues.indexOf(string) != -1)
				{
					// warn default illegal
					LogAs.warning("String field " + Dump.toString(field) + " uses default value of " + Dump.toString(string) + " which is flagged as an illegal value.\r\rjson:" + Dump.toString(json), WARNING_DEFAULT_VALUE_NOT_LEGAL);
				}
				else if (legalValues && legalValues.indexOf(defaultValue) == -1)
				{
					// warn default not legal
					LogAs.warning("String field " + Dump.toString(field) + " uses default value of " + Dump.toString(defaultValue) + " when it should be set to one of the following:" +
						Dump.toString(legalValues) + ".\r\rjson:" + Dump.toString(json), WARNING_DEFAULT_VALUE_NOT_LEGAL);
				}
			}
			else if (illegalValues && illegalValues.indexOf(string) != -1)
			{
				// give warning if string on list of illegal values
				LogAs.warning("String field " + Dump.toString(field) + " = " + Dump.toString(string) + " which is flagged as an illegal value.\r\rjson:" + Dump.toString(json), WARNING_STRING_VALUE_IS_ILLEGAL);
				string = defaultValue;
			}
			else if (legalValues && legalValues.indexOf(string) == -1)
			{
				// give warning if string not on the list of legal values
				LogAs.warning("String field " + Dump.toString(field) + " = " + Dump.toString(string) + " when it should be set to one of the following:" + 
					Dump.toString(legalValues) + ".\r\rjson:" + Dump.toString(json), WARNING_STRING_VALUE_NOT_LEGAL);
				string = defaultValue;
			}
			
			return string;
		}
		
        
        // returns "true", "false", or null, if no Boolean was found.
        public static function getBooleanAsString(json:Object, 
                                                  field:String, 
                                                  required:Boolean = false):String
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.getBooleanAsString() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return null;
            }
            
            var string:String = json[field];
            if (string) 
			{
                string = string.toLowerCase();
                if (string == TRUE || string == FALSE) 
                {
                    return string;
                } 
                else 
                {
                    return null;
                }
            }
            
            if (required) 
            {
                LogAs.warning("Could not find required Boolean field " + Dump.toString(field) + ".\r\rjson:" + 
                    Dump.toString(json), WARNING_CANT_GET_REQUIRED_BOOLEAN);
            }
            
            return null;
        }
        
        
        public static function getBoolean(json:Object, 
                                          field:String, 
                                          required:Boolean = false,
                                          defaultValue:Boolean = false):Boolean
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.getBoolean() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return defaultValue;
            }
            
            var string:String = json[field];
            if (string) 
            {
                string = string.toLowerCase();
                
                if (string == TRUE) 
                {
                    return true;
                } 
                else if (string == FALSE) 
                {
                    return false;
                } 
                else 
                {
                    return defaultValue;
                }
            }
            
            if (required) 
            {
                LogAs.warning("Could not find required Boolean field " + Dump.toString(field) + ".\r\rjson:" + 
                    Dump.toString(json), WARNING_CANT_GET_REQUIRED_BOOLEAN);
            }
            return defaultValue;
        }
        
        public static function getNumber(json:Object, 
                                         field:String, 
                                         required:Boolean = false,
                                         defaultValue:Number = NaN,
                                         minimum:Number = NaN,
                                         maximum:Number = NaN):Number
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.getNumber() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return defaultValue;
            }
            
            var number:Number;
            var string:String = json[field];
            if (! string) 
            {
                if (required) 
                {
                    LogAs.warning("Could not find required Number field " + Dump.toString(field) + ".\r\rjson:" + 
                        Dump.toString(json), WARNING_CANT_GET_REQUIRED_NUMBER);
                }
                number = defaultValue;
            } 
            else 
            {
                number = json[field];
            }
            
            if (! isNaN(minimum) && number < minimum) 
            {
                LogAs.warning("Number field " + Dump.toString(field) + " = " + number + " when it shouldn't be < " + minimum + ".\r\rjson:" + 
                    Dump.toString(json), WARNING_NUMBER_BELOW_MINIMUM);
                number = minimum;
            }
            
            if (! isNaN(maximum) && number > maximum) 
            {
                LogAs.warning("Number field " + Dump.toString(field) + " = " + number + " when it shouldn't be > " + maximum + ".\r\rjson:" + 
                    Dump.toString(json), WARNING_NUMBER_EXCEEDS_MAXIMUM);
                number = maximum;
            }
            
            return number;
        }
        
        // TODO: getInt()
        
        public static function getUint(json:Object, 
                                       field:String, 
                                       required:Boolean = false,
                                       defaultValue:uint = 0,
                                       maximum:Number = NaN,
                                       minimum:Number = NaN):uint
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.getUint() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return defaultValue;
            }
            
            var string:String = json[field];
            if (! string) 
            {
                if (required) 
                {
                    LogAs.warning("Could not find required Number field " + Dump.toString(field) + ".\r\rjson:" + 
                        Dump.toString(json), WARNING_CANT_GET_REQUIRED_NUMBER);
                }
                return defaultValue;
            }
            
            var uintValue:uint = json[field];
            var number:Number = json[field];
            
            if (number < 0) 
            {
                LogAs.warning("uint field " + Dump.toString(field) + " = " + number + ". A uint should not be negative.\r\rjson:" + 
                    Dump.toString(json), WARNING_UINT_PASSED_AS_NEGATIVE);
                uintValue = Math.abs(number);
            }
            
            return uintValue;
        }
        
        public static function getObject(json:Object, 
                                         field:String, 
                                         required:Boolean = false, 
                                         defaultValue:* = null):*
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.getObject() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return defaultValue;
            }
            
            var value:Object = json[field];
            if (! value) 
            {
                if (required) 
                {
                    LogAs.warning("Could not find required field " + Dump.toString(field) + ".\r\rjson:" + 
                        Dump.toString(json), WARNING_CANT_GET_REQUIRED_FIELD, true);
                }
                
                return defaultValue;
            }
            
            return unpackValue(value);
        }
        
        
        private static function unpackValue(value:*):*
        {
            if (value is Array) 
            {
                var array:Array = value as Array;
                value = array.map(function (element:*):* 
				{ 
					return unpackValue(element); 
				});
            } 
            else if (Dump.isDynamicObject(value)) 
            {
                var unpackedObject:* = ClassPacker.fromJson(value, false);
                if (unpackedObject) 
                {
                    value = unpackedObject;
                }
            }
            
            return value;
        }
        
        /**
         * sometimes it's desirable to strip empty values
         * such as null, 0, NaN, false and ""
         * 
         * By default this method will also strip empty Arrays [] and empty Objects {}
         */
        public static function stripEmptyValues(json:Object, 
                                                stripEmptyArraysAndObjects:Boolean = true):Object
        {
            var counter:uint = 0;
            for (var key:String in json) 
            {
                var value:* = json[key];
                
                if (! value ||
                    (value is String && ! Util.stringHasValue(value as String)) ||
                    (value is Number && isNaN(value as Number)) ) 
				{
                    delete json[key];
                } 
                else if (stripEmptyArraysAndObjects) 
                {
                    // check for empty Objects or Arrays
                    if ((Dump.isDynamicObject(value) && Dump.dynamicObjectHasValue(value)) ||
                        (value is Array && ! (value as Array).length)) 
					{
                        delete json[key];
                    }
                }
            }
            return json;
        }
        
        
        
        /**
         * Gives a warning if trying to add a field to JSON and that field already exists.
         * 
         * @param json JSON object being edited
         * @param field Name of field in json being added
         * @param value Value field is being set to
         * @param replaceWithValue If false, any existing value for field is not overwritten
         * @param giveWarning If true, warning log is given if field already exists.
         * @return Modified JSON object is returned
         */
        public static function setValue(json:Object, 
                                        field:String, 
                                        value:*, 
                                        replaceWithValue:Boolean = true, 
                                        giveWarning:Boolean = true):Object
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json && giveWarning) 
                {
                    LogAs.warning("JsonValidator.setValue() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return null;
            }
            
            if (json[field]) 
            {
                if (giveWarning) 
                {
                    LogAs.warning("Field " + Dump.toString(field) + " about to be set to " + Dump.toString(value) + 
                        " already exists with value of " + Dump.toString(json[field]) + 
                        ".\r\rjson:" + Dump.toString(json), WARNING_VARIABLE_BEING_SET_ALREADY_EXISTS);
                }
                
                if (! replaceWithValue)
                {
                    return json;
                }
            }
            
            json[field] = value;
            
            return json;
        }
        
        
        public static function setObject(json:Object,
                                         field:String,
                                         instance:*,
                                         constructorParameters:Array = null, 
                                         giveWarning:Boolean = true):Object
        {
            if (! Dump.isDynamicObject(json)) 
            {
                if (json) 
                {
                    LogAs.warning("JsonValidator.setObject() received invalid JSON Object of type:" + getQualifiedClassName(json) + ".\r\r value:\r" + Dump.toString(json), WARNING_OBJECT_NOT_JSON, true);
                }
                return null;
            }
            
            var packedObject:Object = ClassPacker.toJson(instance, false, constructorParameters);
            return setValue(json, field, packedObject, true, giveWarning); 
        }
    }
}